package org.ant.handler;

import java.util.ArrayList;

import javax.swing.tree.DefaultMutableTreeNode;

import org.ant.ctrl.CodeErrorManager;
import org.ant.handler.typeChecking.ClassDesc;
import org.ant.handler.typeChecking.MethodDesc;
import org.ant.handler.typeChecking.MethodParameter;
import org.ant.handler.typeChecking.VarDesc;
import org.ant.parser.ASTExp;
import org.ant.parser.ASTExpList;
import org.ant.parser.ASTFormalList;
import org.ant.parser.ASTId;
import org.ant.parser.ASTMethodDecl;
import org.ant.parser.ASTOp;
import org.ant.parser.ASTResultType;
import org.ant.parser.ASTStatement;
import org.ant.parser.ASTTerminal;
import org.ant.parser.ASTType;
import org.ant.parser.ASTVarDecl;
import org.ant.parser.Node;


public class SymbolTableHandler {

	private ArrayList<ClassDesc> classList;
	private ArrayList<String> usedTypeList;
	private ArrayList<String> defineTypeList;
	private ClassDesc currentClass;
	private MethodDesc currentMethod;


	public SymbolTableHandler(){
		classList = new ArrayList<ClassDesc>();
		usedTypeList = new ArrayList<String>();
		defineTypeList = new ArrayList<String>();
		currentClass = null;
		currentMethod = null;
	}

	public void injectClass(String className,String parent){
		ClassDesc c = new ClassDesc(className,parent);
		classList.add(c);
		currentClass = c;
		currentMethod = null;
	}

	public void injectMethod(String methodName, String returnType,ArrayList<MethodParameter> parameters) throws Exception{
		if (currentClass==null){
			CodeErrorManager.addError("method not in any class");
		}else{
			MethodDesc m = new MethodDesc(methodName,returnType,parameters);
			m.setInClass(currentClass);
			this.currentClass.addMethod(m);
			this.currentMethod = m;
		}
	}

	public void injectVar(String varName, String type) throws Exception{
		if (currentClass==null){
			CodeErrorManager.addError("method not in any class");
		}else if (currentMethod==null){
			VarDesc v = new VarDesc(varName,type);
			v.setInClass(currentClass);
			v.setInMethod(null);
			if (currentClass.getVarByName(varName)!=null){
				CodeErrorManager.addError("duplicate var: "+varName);
			}else{
				this.currentClass.addVariable(v);
			}
		}else{
			VarDesc v = new VarDesc(varName,type);
			v.setInClass(null);
			v.setInMethod(currentMethod);
			
			if (getVar(currentClass.getClassName(),currentMethod.getMethodName(),varName)!=null){
				CodeErrorManager.addError("duplicate var: "+varName);
			}else{
				this.currentMethod.addVariable(v);
			}
			
		}
	}

	public MethodDesc chooseMethod(String className,String methodName,ArrayList<String> paraTypes){
		ClassDesc c = this.getClassByName(className);
		if(c!=null){
			ArrayList<MethodDesc> mlist = c.getMethods().get(methodName);
			if (mlist!=null){
				for (MethodDesc m:mlist){
					if (m.getParameters().size()== paraTypes.size()){
						boolean match = true;
						for(int i=0;i<m.getParameters().size();i++){
							if(!isType(paraTypes.get(i), m.getParameters().get(i).getType())){
								match = false;
							}
						}
						if (match){
							return m;
						}
					}
				}
			}

			if (c.getParent()!=null){
				return chooseMethod(c.getParent(),methodName,paraTypes);
			}
		}
		return null;
	}

	public void exitClass(){
		this.currentClass = null;
	}

	public void exitMethod(){
		this.currentMethod = null;
	}

	public boolean classExist(String className){
		for (ClassDesc cd:classList){
			if (cd.getClassName().equals(className)){
				return true;
			}
		}
		return false;
	}



	public void generateTypeList(){
		for (ClassDesc cd:classList){
			this.defineTypeList.add(cd.getClassName());
		}

		for (ClassDesc cd:classList){
			this.usedTypeList.add(cd.getClassName());
			for (VarDesc v:cd.getVars()){
				addType(v.getType());
			}

			for (ArrayList<MethodDesc> mList:cd.getMethods().values()){
				for (MethodDesc m:mList){
					addType(m.getReturnType());
					for (VarDesc v:m.getVars()){
						addType(v.getType());
					}
					for (MethodParameter p:m.getParameters()){
						addType(p.getType());
					}
				}
			}
		}
	}

	public void checkType(Node node){
		generateTypeList();
		checkExpType(node);
		checkVarType(node);
	}
	public void checkExpType(Node node){
		if (node instanceof ASTExp){
			setExpType((ASTExp)node);
		} else{
			for (int i = 0; i < node.jjtGetNumChildren(); i++) {
				checkExpType(node.jjtGetChild(i));
			}
		}
	}

	public void checkVarType(Node node){
		if (node instanceof ASTStatement){
			ASTStatement statement = (ASTStatement) node;
			if (statement.getName().equals("id = Exp ;")){
				String inClass = statement.getCurrentClass();
				String inMethod = statement.getCurrentMethod();
				ASTId id = (ASTId)statement.jjtGetChild(0);
				ASTExp exp = (ASTExp)statement.jjtGetChild(1);
				VarDesc var = this.getVar(inClass, inMethod,id.getName());
				if (var==null){
					statement.addError("var not defined");
					CodeErrorManager.addError(id.getToken().beginLine+" :var not defined");
				}else if(!this.isType(exp.getType(),var.getType())){
					statement.addError("type error");
					CodeErrorManager.addError(id.getToken().beginLine+" :type error" +exp.getType()+"to"+var.getType());
				}

			}else if (statement.getName().equals("id [ Exp ]= Exp ;")){

			}
		}else if(node instanceof ASTVarDecl){
			ASTVarDecl vd = (ASTVarDecl) node;
			if(!this.typeDefined(vd.getType())){
				vd.addError("no such type");
				CodeErrorManager.addError(vd.getToken().beginLine+"no such type");
			}
			if (vd.jjtGetNumChildren()>=3){
				ASTExp exp = (ASTExp)vd.jjtGetChild(2);
				if (!this.isType(exp.getType(), vd.getType())){
					vd.addError("init type error");
					CodeErrorManager.addError(vd.getToken().beginLine+"init type error");
				}
			}
		}else if(node instanceof ASTMethodDecl){
			ASTMethodDecl md = (ASTMethodDecl) node;
			ASTResultType rt = (ASTResultType)md.jjtGetChild(0);
			ASTFormalList fl = (ASTFormalList)md.jjtGetChild(2);

			String returnType = ((ASTType)rt.jjtGetChild(0)).getName();
			if(!this.typeDefined(returnType)){
				md.addError("no such type"+returnType);
				CodeErrorManager.addError("no such type:"+returnType);
			}
			for (int i = 0; i < fl.jjtGetNumChildren(); i++) {
				String t = ((ASTType)fl.jjtGetChild(i).jjtGetChild(0)).getName();
				if(!this.typeDefined(t)){
					CodeErrorManager.addError("no such type:"+t);
				}
			}
		}
		for (int i = 0; i < node.jjtGetNumChildren(); i++) {
			checkVarType(node.jjtGetChild(i));
		}
	}

	public String setExpType(ASTExp node){
		if (node.isPreOnly()){
			setPreExpType(node);
			if (node.isPreArray()){
				node.setType(node.getPreType());
				node.setArray(true);
			}else{
				node.setType(node.getPreType());
				node.setArray(false);
			}
			
			
		}else{
			setPreExpType(node);
			setPostExpType(node);
		}
		return null;

	}

	public void setPreExpType(ASTExp node){
		if (node.getPrefix().equals("terminal")){
			ASTTerminal child = (ASTTerminal)(node.jjtGetChild(0));
			if (child.getType().equals("id")){
				String inClass = child.getCurrentClass();
				String inMethod = child.getCurrentMethod();
				//System.out.println(inClass+inMethod);
				String id = child.getName();
				VarDesc var = this.getVar(inClass, inMethod,id);
				if (var==null){
					child.addError("var not defined");
					CodeErrorManager.addError("var not defined:"+id);
					node.setPreType("id");
				}else {
					String type = var.getType();
					node.setPreType(type);
					if (type.contains("[]")){
						node.setPreArray(true);
					}
				}
			}else{
				node.setPreType(child.getType());
			}
		}else if (node.getPrefix().equals("new double[exp]")){
			ASTExp child = (ASTExp)(node.jjtGetChild(0).jjtGetChild(0));
			setExpType(child);
			if (!child.getType().equals("int")){
				node.addError("index not int");
				CodeErrorManager.addError("index not int");
			}
			node.setPreType("double[]");
			node.setPreArray(true);
		}else if (node.getPrefix().equals("new int[exp]")){
			ASTExp child = (ASTExp)(node.jjtGetChild(0).jjtGetChild(0));
			setExpType(child);
			if (!child.getType().equals("int")){
				node.addError("index not int");
				CodeErrorManager.addError("index not int");
			}
			node.setPreType("int[]");
			node.setPreArray(true);
		}else if (node.getPrefix().equals("new id()")){
			ASTId child = (ASTId)(node.jjtGetChild(0).jjtGetChild(0));
			if (!classDefined(child.getName())){
				node.addError("class not defined");
				CodeErrorManager.addError("class not defined:"+child.getName());
			}
			node.setPreType(child.getName());
		}else if (node.getPrefix().equals("!exp")){
			ASTExp child = (ASTExp)(node.jjtGetChild(0));
			setExpType(child);
			if (!child.getType().equals("boolean")){
				node.addError("operation ! error");
				CodeErrorManager.addError("operation ! error");
			}
			node.setPreType("boolean");
		}else if (node.getPrefix().equals("(exp)")){
			ASTExp child = (ASTExp)(node.jjtGetChild(0));
			setExpType(child);
			node.setPreType(child.getType());
		}
	}

	public boolean classDefined(String name) {
		for (String t:this.defineTypeList){
			if (t.equals(name)){
				return true;
			}
		}
		return false;
	}

	public boolean typeDefined(String name) {
		if (name.equals("int")||name.equals("boolean")
				||name.equals("double")||name.equals("int[]")
				||name.equals("double[]")){
			return true;
		}else{
			return this.classDefined(name);
		}
	}

	public void setPostExpType(ASTExp node){
		if (node.getPostfix().equals("[exp]")){
			ASTExp child = (ASTExp)(node.jjtGetChild(1));
			setExpType(child);
			if (!child.getType().equals("int")){
				node.addError("index not int");
				CodeErrorManager.addError("index not int");
			}
			if (!node.isPreArray()){
				node.addError("using [] in a non-array");
				CodeErrorManager.addError("using [] in a non-array");
			}
			String arrayType = node.getPreType();
			node.setType(arrayType.substring(0,arrayType.length()-2));
			node.setArray(false);
		}else if (node.getPostfix().equals("op exp")){
			ASTOp op = (ASTOp)(node.jjtGetChild(1));
			ASTExp exp = (ASTExp)(node.jjtGetChild(2));
			checkExpType(exp);
			String resultType = checkOperation(node.getPreType(),node.isPreArray(),op.getName(),exp.getType(),exp.isArray());
			node.setType(resultType);
		}else if (node.getPostfix().equals(".id(expList)")){
			ASTId id = (ASTId)(node.jjtGetChild(1));
			ASTExpList list = (ASTExpList)(node.jjtGetChild(2));
			ArrayList<String> types = new ArrayList<String>();
			for (int i = 0; i < list.jjtGetNumChildren(); i++) {
				ASTExp exp = (ASTExp)list.jjtGetChild(i);
				checkExpType(exp);
				types.add(exp.getType());
			}
			try {
				String returnType = this.chooseMethod(node.getPreType(), id.getName(), types).getReturnType();
				node.setType(returnType);
			} catch (Exception e) {
				node.addError("no method");
			}
		}else if (node.getPostfix().equals(".length")){
			if (!node.isPreArray()){
				node.addError("using .lenth in a non-array");
				CodeErrorManager.addError("using .lenth in a non-array");
			}
			node.setType("int");
			node.setArray(false);
		}
	}


	public String checkOperation(String type1, boolean isArray1, String op,
			String type2, boolean isArray2) {
		// TODO Auto-generated method stub
		if (type1==null||type2==null) return null;
		if (type1.equals("int")&&type2.equals("int")){
			if (op.equals("+")||op.equals("-")||op.equals("*")){
				return "int";
			}
		}
		if ((type1.equals("int")&&type2.equals("double"))||(type1.equals("double")&&type2.equals("int"))){
			if (op.equals("+")||op.equals("-")||op.equals("*")||op.equals("/")){
				return "double";
			}
		}
		if (type1.equals("boolean")&&type2.equals("boolean")){
			if (op.equals("&&")||op.equals("||")||op.equals("|")){
				return "boolean";
			}
		}
		if ((type1.equals("int")||type1.equals("double"))&&(type2.equals("int")||type2.equals("double"))){
			if (op.equals("<")||op.equals("==")||op.equals(">")||op.equals(">=")||op.equals("<=")){
				return "boolean";
			}
		}
		return null;
	}

	public ClassDesc getClassByName(String className){
		for (ClassDesc cd:classList){
			if (cd.getClassName().equals(className)){
				return cd;
			}
		}
		return null;
	}

	public MethodDesc getMethodByAlias(String methodAlias){
		//TODO
		return null;
	}

	public VarDesc getVarByAlias(String varAlias){
		//TODO
		return null;
	}

	public VarDesc getVar(String className,String methodName,String id){
		ClassDesc c = this.getClassByName(className);
		if (c!=null&&c.getMethods()!=null){
			if (methodName!=null&&c.getMethods().get(methodName)!=null){
				for(MethodDesc m:c.getMethods().get(methodName)){
					for (MethodParameter p:m.getParameters()){
						if (p.getName().equals(id)){
							return p;
						}
					}
					VarDesc v1 = m.getVarByName(id);
					if(v1!=null){
						return v1;
					}
				}
			}
			VarDesc v2 = c.getVarByName(id);
			return v2;
		}
		return null;
	}

	public boolean isType(String typeA,String typeB){
		if (typeA==null||typeB==null){
			return false;
		}
		if (typeA.equals(typeB)){
			return true;
		}
		ClassDesc a = getClassByName(typeA);
		ClassDesc b = getClassByName(typeB);
		if (a!=null&&b!=null){
			//TODO
			if(a.getParent()!=null&&a.getParent().equals(typeB)){
				return true;
			}
		}else if(a==null&&b==null){
			if (typeA.equals("int")&&typeB.equals("double")){
				return true;
			}
		}
		return false;
	}

	private void addType(String type){
		if (!typeExist(type)){
			this.usedTypeList.add(type);
		}

	}

	private boolean typeExist(String type){
		for (String t:this.usedTypeList){
			if (t.equals(type)){
				return true;
			}
		}
		return false;
	}

	public ArrayList<ClassDesc> getClassList() {
		return classList;
	}

	public ArrayList<String> getUsedTypeList() {
		return usedTypeList;
	}

	public ArrayList<String> getDefineTypeList() {
		return defineTypeList;
	}

	public ClassDesc getCurrentClass() {
		return currentClass;
	}

	public MethodDesc getCurrentMethod() {
		return currentMethod;
	}


}
